查看原文
其他

走向ARM CPU 1-bit推理的极致道路

NeuralTalk 2022-11-28

知乎作者:十七树

原文链接:https://zhuanlan.zhihu.com/p/158161592

本文已获原作者转载授权

深度学习的端侧学习对轻量化有着极致的追求,从2016到现在,我们看到业界在推理时使用的数据类型位数一降再降:FP32、FP16、INT8……。显而易见,低比特能节省模型占用的存储空间功耗,在某些精心设计的算法上面也能获得性能优势。在这一点上,1-bit毫无疑问是经典计算机上可以做到的极致。

举一个实际的例子,Birealnet18(二值化的Resnet18)用FP32类型存储大概需要45MB,而通过Bolt转换后得到的1-bit+FP16存储只需要占用2.7MB,exciting!

低位数与性能提升的可能

那么,权重位数走向极致,是不是就能带来计算速度的提升呢?答案取决于你使用的硬件

现阶段深度学习的核心计算还是矩阵乘法,这里面涉及到的就是乘法和加法。大多数硬件的ALU可以处理的最小数据类型就是INT8,要是按照严格的矩阵乘法定义,低于8位的数据类型都得先展开到8位来操作(INT4和INT2哭晕在厕所)。但是1-bit可以来个“降维打击”:权重只有0和1,干脆省掉乘法,直接全都做加减法,多么HAPPY。看到这里,你已经领会到Binary Weight Net的精神了,在经典CPU上已经可以靠着加法相比于乘法的优势行走江湖了。

还没有换台的观众们,估计眉头一锁,觉得此事没有那么简单。是的,深度学习早已经皈依了黄老板的皮衣神教,移动端CPU的绝对霸主ARM也推出了SIMD单元。这些个SIMD单元,乘法和加法用的就是同一个单元,甚至可以用一个指令完成乘加操作。

在现有的SIMD上,1-bit要想占有一席之地,必须再次发动降维。当我们把 feature map 也二值化了后,乘法的两端只会出现0和1,而最终的乘积也只会是0和1,直接位操作就行了,总有一款适合你,而且还不用改动现有的硬件——这就是BNN的精髓。

试想一下,中低端SoC是不支持FP16和INT8的专门优化的,能找到的估计就是跑FP32的CPU。部署FP32的模型,不仅存储感人,推理时间也会感人。但你要是训出一个BNN模型,存储一下子就下去了,而且还能轻松在原有芯片上打败FP32。有些读者可能在担心精度问题,但BNN已经取得长足的进步了,不搞识别,搞个实时检测还是很有价值的。

如何用Bolt基于ARM NEON进行BNN推理

接下来,本文将会介绍Bolt是怎样基于ARM NEON进行BNN推理的

怎样基于ARM NEON进行BNN推理的。在这个事情上,Bolt没有抢到先发,我们被大缺弦老师的BNN框架(daBNN)截胡了。但我们也要感谢大老师选择了开源,特别是把他训练的birealnet18模型也放到了repo上,做BNN框架才有了真正意义上的benchmark。

在我们调试该网络的过程中,他也知无不言,衷心感谢(https://github.com/JDAI-CV/dabnn)。

在一款搭载A55小核心的开发板上,daBNN实测birealnet18(带stem优化版本)单张图片是243ms,而Bolt的单张推理时间是78ms——对于这个结果我们当时也是很震惊的。

关于Bolt相比daBNN的性能提升原因,大家容易想到的第一个因素是在模型的浮点部分,Bolt用的是FP16啊,性能优势是这样来的吗?但是,我们最初开始开发的时候,直接把整个网络按FP16来跑,也需要160ms。其实,在A55小核心上,由于关键指令无法双发射,daBNN是在客场作战的——Bolt几乎所有汇编kernel都有针对A55进行指令发射的优化(注重能耗的开发者记得pick我们)。那么,性能对比在A76上又如何呢?请容许我卖个关子。


接下来先进入技术解密,看完你就更能理解Bolt BNN推理为什么有这么大的优势了。


技术解密:ARM CPU做BNN推理的基本思路

正如大老师在他的论文中讲的,ARM上面做BNN推理最重要的是以下两个指令:

  • AND:逻辑门位操作,要是使用XNORnet那就换一条逻辑门指令;

  • CNT:在向量寄存器上每8个比特统计1的数目,得到0到8之间的数字,按照UINT8存回到那8个比特上

这两条指令应该怎么理解呢?在64位的芯片上,向量寄存器的宽度是128bit。一条AND指令就可以完成128个乘法,而一条CNT指令就完成了16组8个数字的加法。换言之,这两条指令就相当于16组8x8的向量内积

ARM CPU做BNN推理:daBNN的做法及Bolt的改进

那么,接下来就要说到Bolt和daBNN的重要区别了。大老师的128bit装的都是对应同一个输出通道的操作数,所以16组8x8内积得到的16个UINT8数字,还得通过ADDV指令加到一起,然后才加到对应的输出位置上,并行度突然就砍掉了。大老师也发现了ADDV指令是很吃亏的,于是他尽可能先把很多组16个UINT8数字通过向量ADD加起来——毕竟UINT8最大能存255嘛,只用8很浪费——然后再用ADDV。

但是,大家有没有想过可以完全省掉ADDV呢?AND+CNT输出的16个数字,要是对应的是16个输出通道,那我们就只需要直接ADD起来了。这其实是经典的gemm tiling思想,就不在此赘述了。

可能有课代表发现了,直接ADD起来,顶天也就只能加到255啊,不可能一路ADD下去。是的,BNN卷积的结果大多数情况至少也要用INT16才能存储,我们的确也是需要累加到INT16向量的。跟上面一样的道理,为了节省INT16累加的指令,我们需要在ADD的时候尽量用满UINT8的空间。理论上,8x8内积最多产生数字8,而UINT8最大能存255,也就是最多约可以累加32次追求极致的我们需要想这个32怎么来

卷积的时候的每一组加法次数,等于ic*fh*fw(input_channel*filter_h*filter_w)。在Bolt中,我们要求BNN卷积层的输入通道数必须是32的倍数,每次8x8内积会用掉ic里的8,那么ic/8我们只能保证是4的倍数,距离32还差一个8。这个8要怎么来呢?欢迎大家想一想

daBNN和Bolt的性能对比

图 1-bit推理性能时间(ms)对比

感谢大老师放出来的模型,然后测试平台是麒麟990。由于stem版模型的onnx比较晚才放出来,做这张图的时候就没加上去。Bolt+stem在A55上是70.9ms,而在A76上是17.0ms

欢迎加入Noah

感谢大家能够看到这里。Bolt里面的每一个kernel,每一个精度,甚至每一个功能,都经历了如上所示的严密推敲和不断优化,欢迎大家多多体验,这也是我们开源的初衷。大家如果想到了更好的做法,那就太好了,贡献出来,我们一起走向极致。

相关成果我们已经开源到Github:https://github.com/huawei-noah/bolt,方便大家复现和使用,欢迎大家试用反馈体验效果,在社区积极讨论。

高性能,精确,轻量级,易用和安全是我们不懈追求的目标,未来我们会继续利用高性能计算优化技术和编译技术,加速深度学习,在计算机视觉和自然语言处理领域发力,将更多的研究结果带到社区。

HUAWEI Noah's Ark Lab

欢迎高性能计算和深度学习算法方向的童鞋加入我们,获取简历投递邮箱,请在后台回复【华为诺亚】获取。

点击【阅读原文】跳转知乎,一起关注 @十七树

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存